/*
  Copyright 2011 The Rhizosphere Authors. All Rights Reserved.

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
*/

package com.rhizospherejs.gwt.client.gviz;

import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.dom.client.Element;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.visualization.client.AbstractDataTable;
import com.google.gwt.visualization.client.AbstractDrawOptions;
import com.google.gwt.visualization.client.events.Handler;
import com.google.gwt.visualization.client.events.ReadyHandler;
import com.google.gwt.visualization.client.visualizations.Visualization;

import com.rhizospherejs.gwt.client.RhizosphereOptions;
import com.rhizospherejs.gwt.client.RhizosphereRenderer;
import com.rhizospherejs.gwt.client.gviz.GVizRhizosphere.Options;
import com.rhizospherejs.gwt.client.renderer.NativeRenderer;
import com.rhizospherejs.gwt.client.renderer.WidgetBridge;

import java.util.Collection;
import java.util.HashSet;

/**
 * Rhizosphere visualization conforming to the Google Visualization APIs for
 * GWT.
 * <p>
 * This class exposes that same visualization widget as
 * {@link com.rhizospherejs.gwt.client.Rhizosphere} but through the Google
 * Visualization APIs.
 * <p>
 * Rhizosphere visualizes collections of objects. When Rhizosphere loads data
 * from a Google Visualization DataTable it assumes the following:
 * <ul>
 * <li>Each row in the table identifies a datapoint out of the entire dataset
 *   and will be rendered as a separate visual element in the visualization.
 *   This is called a <em>model</em> in Rhizosphere terminology.
 *   {@link DataTableModel} encapsulates this definition.
 * </li>
 * <li>Each column defines an attribute of the datapoints.</li>
 * </ul>
 * <p>
 * These assumptions work best when each row of your DataTable represents an
 * identifiable data point, with given attributes, out of the entire set defined
 * by the DataTable.
 * <p>
 * The metadata associated to the DataTable columns (containing information such
 * as the column label and data type) concur to define the Rhizosphere
 * <em>metamodel</em>. Each DataTable column defines a single
 * {@link com.rhizospherejs.gwt.client.RhizosphereMetaModel.Attribute} of the
 * visualization {@link com.rhizospherejs.gwt.client.RhizosphereMetaModel},
 * keyed by the DataTable column ID (if the DataTable columns do not define
 * a column id, a synthetic metamodel attribute name is generated by
 * Rhizosphere). Use column IDs to customize the visualization metamodel as
 * in this example:
 * <pre><code>
 * DataTable data = DataTable.create();
 * data.addColumn(ColumnType.STRING, "Task", "task");
 * data.addColumn(ColumnType.NUMBER, "Hours per Day", "hours");
 * data.addRows(2);
 * data.setValue(0, 0, "Work");
 * data.setValue(0, 1, 14);
 * data.setValue(1, 0, "Sleep");
 * data.setValue(1, 1, 10);
 *
 * RhizosphereOptions&lt;DataTableModel&gt; options = RhizosphereOptions.create();
 * RhizosphereMetaModel meta = RhizosphereMetaModel.create();
 * meta.newAttribute("hours").
 *     setLabel("Hours per Day").
 *     setKind(RhizosphereKind.RANGE).
 *     setRange(5, 20, 0, 0);
 * options.setMetaModelFragment(meta);
 *
 * RooPanel.get().add(new GVizRhizosphere(data, options));
 * </code></pre>
 * <p>
 * Before attaching the visualization to the DOM you must ensure that the
 * javascript libraries Rhizosphere depends upon have already been
 * injected in the DOM (see
 * {@link com.rhizospherejs.gwt.client.RhizosphereLoader}). This is in addition
 * to loading the required Google Visualization libraries. The visualization
 * initialization code will therefore be similar to the following:
 * <pre><code>
 * com.google.gwt.visualization.client.VisualizationUtils.loadVisualizationApi(new Runnable() {
 *   &#064;Override
 *   public void run() {
 *     // Google Visualization APIs loaded.
 *     RhizosphereLoader.getInstance().ensureInjected(new Runnable() {
 *       &#064;Override
 *       public void run() {
 *         // Rhizosphere APIs loaded.
 *         AbstractDataTable table = createDataTable();
 *         RhizosphereOptions&lt;DataTableModel&gt; options = createOptions();
 *         GVizRhizosphere viz = new GVizRhizosphere(table, options);
 *         viz.setWidth("600px");
 *         viz.setHeight("400px");
 *         viz.addReadyHandler(new ReadyHandler() {
 *           &#064;Override
 *           public void onReady(ReadyEvent event) {
 *             GWT.log("Rhizosphere Visualization is ready");
 *           }
 *         });
 *         RootPanel.get().add(viz);
 *       }
 *     });
 *   }
 * });
 * </code></pre>
 *
 * @see <a target="_blank"
 *         href="http://code.google.com/p/gwt-google-apis/wiki/VisualizationGettingStarted">
 *         Google Visualization APIs for GWT</a>
 * @author battlehorse@google.com (Riccardo Govoni)
 * @author dinoderek@google.com (Dino Derek Hughes)
 */
public class GVizRhizosphere extends Visualization<Options> {

  /**
   * Defines a Rhizosphere <em>model</em> when the input data for Rhizosphere
   * is a Google Visualization DataTable. In this context, a single row of the
   * input DataTable defines a datapoint of the Rhizosphere visualization.
   */
  public static class DataTableModel {
    private AbstractDataTable dataTable;
    private int row;

    /**
     * Creates a new model instance.
     *
     * @param dataTable The input datatable that is the source of data for the
     *     visualization.
     * @param row The datatable row this model represents.
     */
    public DataTableModel(AbstractDataTable dataTable, int row) {
      this.dataTable = dataTable;
      this.row = row;
    }

    public AbstractDataTable getDataTable() {
      return dataTable;
    }
    public int getRow() {
      return row;
    }
  }
  /**
   * A specialized renderer interface tailored to render Rhizosphere
   * <em>models</em> as defined in the context of a Rhizosphere visualization
   * whose input data is a Google Visualization Datatable.
   * <p>
   * Custom renderers passed to a {@link GVizRhizosphere} instance via
   * configuration options must implement this interface.
   *
   * @see DataTableModel
   */
  public interface GVizRenderer extends RhizosphereRenderer<DataTableModel> {}

  /**
   * Configuration options for the visualization.
   * <p>
   * Use {@link RhizosphereOptions} to define configuration options and then
   * invoke the {@link #wrap(RhizosphereOptions)} method to convert them to
   * the proper type required by the Google Visualization APIs.
   */
  public static class Options extends AbstractDrawOptions {
    protected Options() {}

    /**
     * Converts a {@link RhizosphereOptions} instance to a type compatible
     * with the Google Visualization APIs.
     *
     * @param options The options to convert.
     * @return A set of configuration options wrapped in a type compatible with
     *     the Google Visualization API.
     */
    public static Options wrap(RhizosphereOptions<DataTableModel> options) {
      return options.cast();
    }

    private RhizosphereOptions<DataTableModel> asRhizosphereOptions() {
      return this.cast();
    }
  }

  /**
   * Intermediary panel that wrap widgets emitted by custom
   * {@link GVizRenderer} instances (if specified in the visualization options).
   * <p>
   * The purpose of this panel is to expose hooks of the GWT widget lifecycle
   * so that Rhizosphere renderings (which are widgets half managed by GWT and
   * half managed by Rhizosphere JSNI code) do not leak memory because of
   * missed attach/detach widget cleanups.
   */
  private static class HostingPanel extends SimplePanel {

    public void explicitAttach() {
      onAttach();
    }

    public void explicitDetach() {
      onDetach();
    }
  }

  /**
   * A Widget bridge that manages lifecycle events originated from Rhizosphere
   * renderings attached to/detached from this visualization.
   */
  private static class GVizWidgetBridge implements WidgetBridge {

    private Collection<HostingPanel> widgets = new HashSet<HostingPanel>();

    @Override
    public void add(Widget widget) {
      HostingPanel panel = (HostingPanel) widget;
      panel.explicitAttach();
      widgets.add(panel);
    }

    @Override
    public boolean remove(Widget widget) {
      HostingPanel panel = (HostingPanel) widget;
      panel.explicitDetach();
      widgets.remove(panel);
      return true;
    }

    /**
     * Propagates logical attach notifications from the visualization itself
     * down to all the renderings the visualization manages.
     * This happens, for example, when the entire visualization is moved
     * from one container panel to another.
     */
    public void doAttachChildren() {
      for (HostingPanel p : widgets) {
        p.explicitAttach();
      }
    }

    /**
     * Propagates logical detach notifications from the visualization itself
     * down to all the renderings the visualization manages.
     * This happens, for example, when the entire visualization is moved
     * from one container panel to another.
     */
    public void doDetachChildren() {
      for (HostingPanel p : widgets) {
        p.explicitDetach();
      }
    }

    /**
     * Wraps each widget produced by a {@link GVizRenderer} into an
     * {@link HostingPanel} to hook into logical attach/detach calls.
     */
    @Override
    public Widget processRendering(Widget widget) {
      HostingPanel p = new HostingPanel();
      p.setWidget(widget);
      return p;
    }
  }

  /**
   * Converts the JavaScriptObject that native Rhizosphere Javascript code
   * uses to represent visualization models into an equivalent instance of
   * {@link DataTableModel} describing the same model.
   */
  private static class ModelExtractor implements
      com.rhizospherejs.gwt.client.bridge.ModelExtractor<DataTableModel> {

    private AbstractDataTable datatable;

    public void setDataTable(AbstractDataTable datatable) {
      this.datatable = datatable;
    }

    @Override
    public DataTableModel extractModel(JavaScriptObject jso) {
      int row = nativeGetDataTableRow(jso);
      return new DataTableModel(datatable, row);
    }

    private native int nativeGetDataTableRow(JavaScriptObject jso) /*-{
      var gvizIdPrefix = 'gviz-';
      if (jso['id'].indexOf(gvizIdPrefix) == 0) {
        return parseInt(jso['id'].substring(gvizIdPrefix.length), 10);
      }
      return -1;
    }-*/;
  }

  private GVizWidgetBridge widgetBridge;
  private ModelExtractor modelExtractor;

  /**
   * Creates a new visualization instance with default options and no data.
   */
  public GVizRhizosphere() {
    super();
    init();
  }

  /**
   * Creates a new visualization instance.
   *
   * @param data The data to visualize.
   * @param options The configuration options. You cannot reuse options between
   *     different visualizations. Each visualization must have a separate
   *     instance of Options.
   */
  public GVizRhizosphere(AbstractDataTable data, Options options) {
    super(data, options);
    init();
  }

  /**
   * Creates a new visualization instance.
   *
   * @param data The data to visualize.
   * @param options The configuration options. You cannot reuse options between
   *     different visualizations. Each visualization must have a separate
   *     instance of RhizosphereOptions.
   */
  public GVizRhizosphere(AbstractDataTable data, RhizosphereOptions<DataTableModel> options) {
    super(data, Options.wrap(options));
    init();
  }

  private void init() {
    widgetBridge = new GVizWidgetBridge();
    modelExtractor = new ModelExtractor();
  }

  /**
   * Adds an handler that will be notified when the visualization is ready for
   * user interaction.
   *
   * @param handler The handler to add.
   */
  public final void addReadyHandler(ReadyHandler handler) {
    Handler.addHandler(this, "ready", handler);
  }
  
  /**
   * Adds an handler that will be notified whenever errors occur within the
   * visualization.
   *
   * @param handler The handler to add.
   */
  public final void addErrorHandler(ErrorHandler handler) {
    Handler.addHandler(this, "error", handler);
  }

  /**
   * Creates the native Javascript object that builds the Rhizosphere
   * visualization.
   * <p>
   * The returned object is a subclass of {@code rhizo.gviz.Rhizosphere} instead
   * of {@code rhizo.gviz.Rhizosphere} itself because we must trap
   * {@code draw()} before they are processed by the visualization. Since the
   * visualization options can define a custom renderer and since the renderer
   * must be explicitly bound to the visualization widget (to guarantee a
   * correct lifecycle for all the widgets the renderer generates), we must
   * trap and process the visualization options before they are used.
   * <p>
   * Additionally, instead of attaching the visualization to the input div
   * element, we define a subelement to have control over its positioning.
   * Setting the element position style to 'relative' causes it to be treated
   * as a new positioning context for its children (a requirement because
   * Rhizosphere internal elements rely on absolute positioning).
   */
  @Override
  protected final native JavaScriptObject createJso(Element div) /*-{
    var gwtviz = this; 
    return (function() {
      var relDiv = $doc.createElement('div');
      relDiv.style.position = 'relative';
      relDiv.style.width = '100%';
      relDiv.style.height = '100%';
      div.appendChild(relDiv);

      var Wrapper = function(div) {
        $wnd.rhizo.gviz.Rhizosphere.call(this, div);
      };
      $wnd.rhizo.inherits(Wrapper, $wnd.rhizo.gviz.Rhizosphere);

      Wrapper.prototype.draw = function(datatable, options) {
        gwtviz.@com.rhizospherejs.gwt.client.gviz.GVizRhizosphere::drawTrap(Lcom/google/gwt/visualization/client/AbstractDataTable;Lcom/rhizospherejs/gwt/client/gviz/GVizRhizosphere$Options;)(datatable, options);
        $wnd.rhizo.gviz.Rhizosphere.prototype.draw.call(this, datatable, options);
      };

      return new Wrapper(relDiv);
    })();
  }-*/;

  @SuppressWarnings("unused")
  private void drawTrap(AbstractDataTable datatable, Options options) {
    modelExtractor.setDataTable(datatable);
    NativeRenderer<DataTableModel> nativeRenderer =
        options.asRhizosphereOptions().getNativeRenderer();
    if (nativeRenderer != null) {
      nativeRenderer.setModelExtractor(modelExtractor);
      nativeRenderer.setWidgetBridge(widgetBridge);
    }
  }

  /**
   * Propagates widget attach notifications to all the existing renderings of
   * Rhizosphere models (that they would otherwise miss since they are only
   * physically connected to the visualization via the DOM, but not logically
   * connected).
   */
  @Override
  protected void doAttachChildren() {
    widgetBridge.doAttachChildren();
  }

  /**
   * Propagates widget detach notifications to all the existing renderings of
   * Rhizosphere models (that they would otherwise miss since they are only
   * physically connected to the visualization via the DOM, but not logically
   * connected).
   */
  @Override
  protected void doDetachChildren() {
    widgetBridge.doDetachChildren();
  }
}
